本文基于完整开发过程整理,力求还原每一处产品设计与技术实现的细节,供记忆与理念深挖。
<!–
目录
- 零、技术理念与产品哲学的映射
- 一、产品定位与架构概览
- 二、启动与认证体系
- 三、首页信息流
- 四、办展与展览系统
- 五、笔记内容生态
- 六、音频展览
- 七、个人主页与社交
- 八、设置与系统
- 九、扫码功能
- 十、设计与交互细节汇总
- 十一、全局实体状态管理与跨页面同步
- 十二、音频焦点与全局媒体协调
- 十三、展览封面解析策略
- 十四、体现设计用心的修复与优化
- 十五、路由体系
- 十六、设计系统组件
- 十七、埋点与分析
- 十八、分享系统
–>

零、技术理念与产品哲学的映射
| 技术实现 | 产品/设计理念 |
|---|---|
| 「一分钟创建展览」的快速布展 | 降低策展门槛,让艺术表达平民化 |
| 展厅透视+画框材质渲染 | 线上展览不是简单图片排列,而是空间与审美的还原 |
| 视频/音频/图文的多媒体混合 | 不同内容形态适配不同表达方式 |
| 笔记两级浏览(预览→全文) | 尊重用户注意力,先吸引再深入 |
| 背景音乐与视频的音频焦点管理 | 细节处的沉浸感打磨 |
| 邀约办展机制 | 平台对优质内容的筛选与背书 |
| 实名认证前置 | 社区治理与内容可信度的基础建设 |
| thumbnailLevel 分级加载 | 性能与体验的平衡,不因省流量牺牲画质 |
一、产品定位与架构概览
「一起展」是一款围绕展览、笔记、生活秀构建的内容社区与创作工具型 App。用户既可以浏览他人发布的展览与图文笔记,也可以通过「一分钟创建展览」快速搭建自己的线上展厅,形成从内容消费到内容生产的完整闭环。
核心架构特征:
- 模块化 Monorepo:采用
apps/host_app壳工程 +packages/features/*业务包拆分,严格遵循单向依赖(apps → features → common → core),业务包之间绝对隔离。 - 状态管理:全面使用
flutter_riverpod(StateNotifierProvider、AsyncNotifierProvider、StateProvider),已完成从 Provider 的迁移。 - 路由体系:基于
go_router的统一命名路由,所有跨模块跳转通过core_router实现,禁止直接 import 目标页面类文件。 - 媒体处理:视频使用
video_player,音频使用just_audio,图片使用CachedNetworkImage,本地媒体选取使用image_picker。 - 数据埋点:集成 Matomo 分析,页面进入、按钮点击、视频播放、发布漏斗等关键行为均带
matomoTitle上报。
二、启动与认证体系
2.1 启动页(Splash)
- 纯品牌展示页,作为应用入口;完成后根据登录态决定跳转登录页或首页。
2.2 登录页(LoginPage)
设计亮点:
- 沉浸式登录氛围:页面顶部使用全宽品牌背景图(
assets/icons/login/login_bg.png),下方承载登录表单,视觉层次分明。 - 双通道登录:支持「手机号 + 短信验证码」与「微信授权登录」两种方式。
- 手机号输入细节:
- 左侧固定展示
+86区号选择器; - 输入框底部边框随焦点状态动态变色(聚焦高亮蓝色,失焦灰色);
- 支持一键清空输入(右侧
clear图标); - 输入限制:仅数字、最多 11 位。
- 左侧固定展示
- 验证码输入细节:
- 与手机号输入框共享一致的设计语言;
- 右侧「获取验证码」按钮带倒计时状态(
countdown),发送中展示CircularProgressIndicator; - 倒计时期间按钮置灰并显示剩余秒数。
- 协议同意机制:登录按钮下方放置勾选框,必须勾选《用户协议》与《隐私政策》才能触发登录;文案可点击跳转对应协议页。未勾选时点击登录会弹出
SnackBar提示。 - 登录按钮:紫蓝渐变背景(
#6A11CB → #2575FC),宽度铺满,圆角 8px;仅当手机号 11 位且验证码 6 位时才可点击。 - 其他登录方式区:独立区块展示「其他登录方式」文案,居中放置微信图标按钮,点击唤起微信授权。
- 登录后路由:登录成功优先
pop返回上一页;若无上一页则pushReplacementNamed到首页。 - 异常处理:捕获
DioException,对HandshakeException给出明确的「网络证书校验失败」提示,便于调试。
2.3 绑定手机号(BindPhonePage)
- 微信登录后若后端返回
NeedBindPhoneException,强制引导至绑定手机号页,完成后方可进入主流程。
2.4 协议页(AgreementPage)
- 支持《用户协议》与《隐私政策》两种类型,通过
AgreementType枚举区分,纯文本展示。
三、首页信息流(HomePage)
首页是整个 App 的核心流量入口,采用底部导航 + 顶部 Tab 双层结构。
3.1 底部导航栏(4 大板块)
| Tab | 说明 |
|---|---|
| 首页 | 信息流主阵地,含 6 个二级频道 |
| 办展 | 展览分类浏览与快速布展入口 |
| 邀约 | 活动/邀请函相关(预留入口) |
| 我的 | 个人中心与内容管理 |
设计细节:
- 底部导航栏图标与文案配合,选中态有明确视觉反馈。
- 状态栏图标颜色自适应:当位于「视频」频道时,状态栏使用浅色图标(
Brightness.light),其余频道使用深色图标(Brightness.dark),避免从视频/看展页返回后状态栏颜色残留导致白底不可见。 - 页面根节点包裹
AnnotatedRegion<SystemUiOverlayStyle>,确保系统 UI 风格与当前内容一致。
3.2 顶部频道 Tab(6 个频道)
推荐、视频、热展、直通车、关注、上海
3.2.1 推荐频道
- 典型的瀑布流笔记列表,卡片高度不一,形成视觉节奏。
- 笔记卡片包含:封面图/视频、标题、作者头像与昵称、点赞数。
- 支持无限滚动加载,带骨架屏占位(
AppSkeletonHallGrid)。
3.2.2 视频频道(HomeVideoTabPage)
核心体验设计:
- 垂直 PageView 全屏视频流:模拟抖音/快手的沉浸式短视频体验,每个视频占满一屏。
- 自定义滑动物理效果:
_ShortDragPageScrollPhysics允许更短的拖拽距离触发翻页,提升操作跟手性。 - 视频预加载策略:开始播放约 5 秒后预热下一条视频,优先保证滑动切换时减少黑屏与卡顿。
- 交互手势:
- 单击:播放/暂停切换;
- 双击:触发点赞动画(红心浮起);
- 长按:触发倍速播放(2x/3x),松手恢复常速。
- 错误降级:视频加载失败时自动回退到封面图展示,避免黑屏。
- 埋点:自动上报视频观看时长、播放错误率等事件。
3.2.3 热展频道
- 展示热门展览卡片,点击进入展览详情页(
ExhibitViewingPageTemplate1)。 - 展览封面解析有专门策略:优先使用
visitFileId作为海报;若展位首项为图片则海报+首张展位图轮播;若首项为视频则仅展示海报。
3.2.4 直通车 / 关注 / 上海
- 直通车:平台精选或商业推广内容入口。
- 关注:展示已关注用户的动态。
- 上海:基于地域(上海)的内容聚合,体现本地化运营思路。
3.4 搜索入口
- 首页顶部导航栏右侧带有搜索图标,点击触发 Matomo 埋点(
category: 'search', action: 'entry_click', name: 'home_headbar')。 - 搜索功能当前为入口占位,后续可扩展为全局内容搜索(笔记/展览/用户)。
3.5 首页生命周期与数据一致性
- didPopNext 自动刷新:首页监听
RouteObserver,当用户从其他页面(如笔记详情、展览页、编辑页)返回时,自动触发当前 Tab 的数据刷新,确保点赞、关注、删除等操作的结果即时同步到列表。 - 首次加载优化:Tab 切换时首次
invalidate对应 Provider 触发加载;已加载过的 Tab 不重复请求,除非用户手动下拉刷新。 - 状态栏动态适配:首页根节点包裹
AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 使用SystemUiOverlayStyle.light,其余 Tab 使用SystemUiOverlayStyle.dark,避免从视频/看展页返回后状态栏图标颜色残留导致白底不可见。
3.3 笔记创建(HomeCreateNotePage)
设计亮点:
- 媒体选择互斥:图片与视频不能同时存在,选中一类后另一类入口自动隐藏。
- 富文本编辑:支持输入标题与正文,正文支持话题提取与插入(
#话题#形式)。 - 草稿机制:支持保存草稿、加载草稿继续编辑,草稿数量在个人主页展示。
- 隐私分级:「公开可见」改造为一级隐私选择 + 二级联系人选择抽屉:
- 公开 / 互关可见 / 私密
- 部分可见 / 不让谁看:支持搜索联系人、多选、显示已选人数与确认回填。
- 位置标签:支持添加地理位置。
- 发布流程:先上传媒体文件,拿到
fileId后再调用发布接口,失败时保留草稿。
四、办展与展览系统(Exhibit)
这是 App 最具差异化的功能模块,将「线上策展」轻量化,让普通用户也能「一分钟创建展览」。
4.1 办展分类页(ExhibitTabPage)
视觉设计:
- 顶部有大幅 Banner 背景图,文案「一分钟创建展览!」,右侧 CTA「立即办展」。
- Banner 与下方内容区有重叠效果(
exhibitContentTopOverlap),内容区以圆角(exhibitContentTopRadius)裁切浮起,形成卡片层叠感。 - 分类导航:横向滚动的一级分类 Tab,带底部指示器(圆角小条);下方有二级/三级分类 Chip 筛选。
- 邀约角标:部分分类带有「邀约」角标,金橙渐变背景(
#FFD09D → #DE8220),圆角不对称设计(左上/右上/右下圆角 6,左下圆角 2),精致且有辨识度。 - 展馆瀑布流:选中分类后展示对应模板/展馆卡片,支持下拉刷新与上拉加载更多;无数据时展示「暂无展馆」空状态。
- 列表底部:到达末尾时展示趣味文案「恭喜你发现了世界的尽头!🌈」配分割线,缓解无更多内容的失落感。
- 实名提醒弹窗:双击 Banner CTA 区域触发「实名提醒」弹窗,引导用户完成实名认证解锁办展功能。
4.2 办展封面页(ExhibitCoverPageTemplate1)
- 展示单个模板/展馆的封面大图(全景图比例
393:262)、标题、关键词标签(橙色背景#FF8C00半透明)、推荐描述。 - 底部渐变遮罩 +「开始布展」大按钮,深蓝渐变(
#11317C → #1B46AA → #224598)。 - 邀约拦截:若当前模板所属分类标记为
requiresInvitation=true,点击「开始布展」不进入编辑,而是重置底部导航到办展 Tab 并回到首页。
4.3 快速布展(FastExhibitPage)
核心交互设计:
- 多展厅管理:支持最多 6 个展厅,底部横向缩略图列表展示各厅;默认 1 号厅选中,右侧「新增展厅」按钮带加号叠加;仅允许删除非一号厅。
- 展位作品管理:
- 空展位:直接点击唤起媒体选择(图片);
- 已填充展位:点击弹出蒙层,可选择「更换作品」或「编辑查看」;
- 长按/批量选择模式:支持多选后批量删除。
- 展厅切换:底部缩略图点击切换,主舞台实时加载对应展厅图片;空位不再回填默认图,保持真实创作状态。
- 底部操作栏:居中分布「信息」「音乐」「清空」按钮,清晰易触。
- 退出确认:返回时若存在未保存内容,弹窗询问「保存为草稿」或「放弃离开」。
- 状态管理:使用
FastExhibitDraftController(StateNotifier)管理hallDrafts,每个展厅独立维护entryLocalPath与boothLocalPaths。
4.4 发布页(FastExhibitPublishPage)
- 全景预览:上方展示当前选中展厅的透视效果,展位按模板
boothList.length动态渲染(不再固定 5 个图位),支持画框材质、阴影、厚度。 - 展厅缩略图切换:底部横向缩略图可切换预览不同展厅。
- 表单信息:填写展览名称、推荐描述、选择定位、隐私设置。
- 提交逻辑:
- 先调用
saveOrUpdate创建展览(esQuantity = boothList.length * hallCount,状态草稿=1/发布=2); - 拿到
id后二次调用同接口编辑提交id/templateName/recommendDesc/fileId/isType/scopeUserIds/boothList。
- 先调用
- 发布后路由:发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab。
- 动态展位布局:
exhibit_wall_artworks_perspective.dart支持动态展位布局与可选frameDataList/frameMaterialUrls,可渲染边框、阴影、厚度与材质。
4.5 展览浏览页(ExhibitViewingPageTemplate1)
沉浸式看展体验:
- 展厅信息卡:外框自上而下渐变并圆角 24,策展人姓名仅加粗(无蓝色下划线),去除花哨样式保持阅读专注。
- 标题滚动动画:超长标题从右往左单向循环滚动,吸引注意力又不干扰整体布局。
- 展厅分布:去掉伸缩头,仅保留更小缩略图横向列表,选中高亮且隐藏「x号厅」字样,减少信息噪音。
- 主视觉区:图片/视频轮播,视频自动播放,图片支持手动滑动。
- 画框与材质:每个展位作品根据
ExhibitBoothFrameData渲染真实画框效果,含材质贴图、装饰垫颜色、厚度阴影。 - 音乐播放:展厅支持背景音乐,右上角音乐按钮实底设计提高清晰度;进入作品详情或放大预览时自动暂停,返回后恢复,状态严格同步避免失步。
- 弹幕:支持弹幕开关;关闭时以固定高度深色占位(
_DanmakuAreaPlaceholder)避免背景发浅突兀。 - 底部操作栏:左到右渐变背景,点赞/评论/收藏图标使用定制 SVG(
icon-exhibit-like.svg、icon-exhibit-comment.svg);点赞激活态为红色,收藏激活态为金色,视觉反馈鲜明。 - 交互防遮挡:自动预览蒙层限制
bottom为MediaQuery.padding.bottom + 64,避免拦截底部操作栏的点击。 - 点赞状态持久化:通过 Riverpod
noteEntityProvider维护展位点赞状态,离开页面再进入不丢失。
4.6 作品详情页(ExhibitArtworkDetailPageTemplate1)
- 主舞台:在可见区域中垂直居中展示作品,带画框材质渲染;图片未加载时保留最小占位约束,加载完成后释放以自适应宽高比。
- 视频作品:
fileType=2时主舞台缩略预览使用视频播放器(thumbnailLevel=3),支持播放控制;点击「查看超清」进入全屏视频播放器(thumbnailLevel=0原视频)。 - 横向滑动切换:左右滑动手势切换同一展厅内其他作品,过滤掉
fileId为空的展位,仅在有实质内容的展位间跳转。 - 底部缩略图轮播:展厅内多作品时,底部展示缩略图条,点击或滑动切换。
- 作品信息弹窗:点击「信息」按钮从底部弹出
AppModalSheet,毛玻璃效果(blurSigma: 10),展示作品名称、作者、推荐描述。 - 互动操作:点赞、评论、分享;点赞状态实时同步到全局实体仓库。
4.7 展览详情页模板(ExhibitDetailPageTemplate1)
另一种展览浏览形态,更偏向「展览介绍 + 作品概览」:
- 背景沉浸:全屏背景图 + 60% 黑色遮罩,营造展厅暗环境氛围。
- 顶部导航栏:白色返回/分享图标,居中展览标题。
- 主展品轮播:
PageView.builder横向滑动,含 10 张展品图,viewportFraction: 0.9让两侧露出下一张边缘;切换时触发AnimationController缩放动效,增强视觉反馈。 - 缩略图导航:底部横向缩略图列表,选中项放大(45px)并带白色边框(2px),未选中项缩小(35px),点击可跳转对应页面。
- 底部功能栏:5 个功能按钮(详情、播放、VR、全屏、分享),白色半透明玻璃质感背景(
alpha: 0.12,边框alpha: 0.6),圆角 6px。 - 详情弹窗:点击「详情」唤起
DraggableScrollableSheet,初始高度 50%,最小 30%,最大 90%;- 顶部白色圆角面板,带系统指示条(灰色短横线);
- 展厅介绍长文本,首行缩进 18px,段落间距 12px,两端对齐;
- 底部浮起「进入展厅」按钮,橙色渐变(
#FF9022)+ 自定义ClipPath弧形裁切,两侧箭头图标引导点击。
4.9 多种展览详情模板(Type 1 / 2 / 3)
产品意图:为不同展览类型提供差异化的浏览体验,体现「一展一风格」的策展理念。
Type 1(智慧卡片):
- 暖米色背景(
#FFFFFAF0),横向PageView展示 GIF 动图卡片,viewportFraction: 0.85; - 卡片带缩放动效(非当前页缩小至 0.8),营造立体纵深感;
- 底部
BackdropFilter毛玻璃控制栏,含缩略图导航(选中项带橙色边框#FF8F1F)与功能按钮(标签/播放/VR/详情/全屏/分享); - 卡片底部叠加毛玻璃文字层,展示作品名称与作者。
Type 2(雕塑展览):
- 类似 Type 1 的横向轮播,但每张卡片展示雕塑作品大图 + 标题 + 详细描述;
- 更适合静态艺术品的深度介绍。
Type 3(画廊模式):
- 全屏背景图随当前选中作品实时切换,
extendBodyBehindAppBar: true让背景延伸至状态栏; BackdropFilter高斯模糊背景图作为氛围层;- 底部横向缩略图条,选中高亮。
3D 展厅(Exhibition3dPage):
- 当前为占位页(”3D view will be implemented here”),预留 WebGL/3D 引擎接入能力,未来可支持 360° 虚拟展厅漫游。
4.8 评论页(ExhibitArtworkCommentPageTemplate1)
- 评论结构:支持一级评论与二级回复,嵌套展示;子评论默认展示前 2 条,超出时「查看全部 x 条评论」可展开。
- 交互细节:
- 每条评论展示头像、昵称、内容、时间;
- 支持回复、点赞、删除(仅自己的评论);
- 评论点赞数实时更新;
- 时间显示智能格式化(刚刚、x分钟前、x小时前、x天前)。
- 输入框:底部悬浮输入栏,点击唤起全屏键盘输入 Sheet,带圆角与阴影;支持回复指定用户(
hintText: '回复:$replyToNickName')。 - 空状态:无评论时展示「暂无评论,快来抢沙发吧」。
- 加载更多:列表滚动到底部 80px 内自动加载更多。
五、笔记内容生态(Note)
5.1 笔记详情页(NoteDetailPage)
两级浏览架构:
- 一级预览:默认进入
startInPreview=true,展示笔记媒体(图片轮播/视频)+ 基本信息;支持左右滑动浏览图片,双击放大。 - 二级文本页:点击「查看全文」进入完整文本页;拦截系统返回与顶部返回,先回一级预览,再返回列表,保证「一级→二级→全屏」与「全屏→二级→一级」的导航链完整。
- 媒体播放:
- 图片使用
CachedNetworkImage,笔记详情轮播图使用缩略图级别 9; - 视频使用
VideoPlayerController,支持点击播放/暂停、双击进入全屏、长按拖动进度条(scrub); - 首次播放提示「双击可放大播放」,2 秒后自动消失。
- 图片使用
- 背景音乐:笔记详情增加
musicFileId(取data.videoFileId),详情页自动播放背景音乐并支持右上角按钮播放/暂停;使用GlobalMusicService统一音频焦点管理,视频播放时自动暂停背景音乐,视频结束/离开恢复。 - 评论:底部评论区支持点赞、回复、删除;支持下拉刷新。
- 生命周期管理:页面
dispose前暂停所有视频,防止返回上一页后视频继续播放;didPushNext/didPopNext/AppLifecycleState均做媒体暂停/恢复处理。
5.2 笔记评论系统(NoteCommentSection / NoteCommentController)
架构设计:
- 使用
FamilyAsyncNotifier<NoteCommentState, int>,以noteId为 family key,每个笔记的评论数据独立管理。 - 评论结构:支持一级评论与二级回复,嵌套展示;
replyShowCount默认 10,UI 层可对超过 3 条的子评论做折叠处理。 - 分页加载:首次加载 page 1,滚动到底部自动
loadMore();isLoadingMore状态控制加载指示器,避免重复请求;到达末尾展示「没有更多评论了」。 - 评论操作:
- 发表评论:支持一级评论(
parentId = 0)与回复指定用户(parentId = commentId,replyToUserId = targetUserId);发表成功后自动refresh()并同步评论数到全局实体仓库(noteInteractionControllerProvider.updateCommentCount)。 - 删除评论:递归遍历评论树找到目标评论并移除(
removeFromList),同步更新总数;删除后同步全局仓库。 - 点赞/取消点赞(乐观更新):先本地翻转
isLiked与likeCount更新 UI,再调用接口;若接口失败则回滚到旧状态,保证用户操作即时反馈且数据一致性。
- 发表评论:支持一级评论(
- UI 组件:
NoteCommentItem展示头像、昵称、内容、时间、点赞数;支持点击回复、点赞、删除(仅自己的评论)。 - 空状态:「暂无评论,快来抢沙发吧」。
5.3 图片全屏浏览(NoteImageGalleryPage)
- 支持多图左右滑动查看,全屏黑色背景,沉浸体验。
六、音频展览(Audio Exhibit)
6.1 音频展览发布(AudioExhibitPublishPage)
- 海报上传区:虚线边框(
DashedBorder)设计,点击上传海报图片;上传后支持替换。 - 音视频上传:支持选择音频或视频文件;视频上传后展示预览,含播放/暂停与进度条。
- 富文本表单:标题、话题标签、简介、脚本;话题支持
#提取。 - 底部发布栏:固定在底部,「发布」按钮带渐变背景。
6.2 音频展览播放(AudioExhibitViewPage)
- 沉浸式播放器设计:
- 背景:基于封面图的
BackdropFilter高斯模糊(sigmaX: 30, sigmaY: 30),营造氛围感; - 中央:圆角封面卡片(
ClipRRect+BoxShadow),悬浮于模糊背景之上; - 信息卡:标题、艺术家名、关注按钮;
- 底部:播放进度条、播放/暂停按钮、评论与歌单按钮。
- 背景:基于封面图的
- 状态管理:使用
GlobalMusicHandle管理音频播放状态,支持全局暂停/恢复。
七、个人主页与社交(MyPage / UserProfilePage)
7.1 个人主页结构
- 顶部 Header:全宽背景图 +
BackdropFilter毛玻璃效果(sigmaX: 30, sigmaY: 30)+ 15% 黑色遮罩;内容区展示头像、昵称、ID、个性签名、关注/粉丝/获赞/收藏数据。 - 头像预览:点击头像进入全屏
PhotoView,支持双指缩放。 - Tab 导航(5 个 Tab):笔记、赞过、收藏、展览、生活秀。
- 瀑布流展示:笔记采用
MasonryGridView双列瀑布流;展览/生活秀采用固定比例网格(177:181)。 - 数据懒加载:Tab 切换时首次加载对应数据,滚动到底部 200px 内自动加载更多。
- 刷新机制:下拉刷新联动当前 Tab 与用户信息;
didPopNext与AppLifecycleState.resumed自动刷新用户数据。 - 展览封面轮播:个人主页展览卡片若有多张图,自动 3 秒轮播,带动画过渡(
animateToPage, 600ms,easeInOut)。 - 社交互动:支持关注/取消关注、私信入口(聊天功能开发中占位)。
7.2 编辑资料(HomeEditProfilePage)
- 头像上传:点击头像唤起图片选择,支持拍照/相册;右下角悬浮相机图标(白色圆形+阴影)。
- 昵称与个性签名输入:表单分组设计,标签 + 圆角输入框(
borderRadius: 14);签名区最小高度 110,多行输入。 - 保存按钮:主色背景,带加载态(
CircularProgressIndicator)。
7.3 生活秀(Life Show)
产品定位:生活秀是展览的一种轻量表达形态,用户可以将日常创作、活动记录以展览形式呈现,降低正式策展的仪式感门槛。
展示形态:
- 列表卡片(LifeShowList):纵向列表,每项为圆角 12px 白色卡片,带轻微阴影(
0x0A000000)。- 顶部封面图(高度 180,铺满圆角裁切),加载失败展示灰色占位图标;
- 内容区:红色标签(
special,如「热门」「精选」)+ 地理位置文案; - 展览名称(单行截断,16px 加粗);
- 推荐描述(两行截断,浅灰色);
- 底部作者行:作者头像(32px 圆形)+ 昵称 + 点赞/评论/收藏统计图标;
- 展期信息(
startTime - overTime)。
- 网格卡片(MyLifeShowGrid):个人主页「生活秀」Tab 采用双列网格(
childAspectRatio: 177/181)。- 封面图铺满,底部渐变遮罩(
#00000000 → #99000000)承载标题与位置; - 右上角红色角标(
special); - 右下角悬浮统计胶囊(半透明黑底圆角 10,点赞/评论数)。
- 封面图铺满,底部渐变遮罩(
- 数据模型:
LifeShowItem包含完整的展览字段(boothList、keywords、author、likeCount、isLiked等),支持点赞、收藏、评论的完整交互。
7.4 草稿箱(HomeDraftListPage)
双 Tab 结构:笔记草稿 + 展览草稿
笔记草稿:
- 使用
_MyNotesGrid网格展示(与「笔记」Tab 一致),支持点击继续编辑; - 点击后进入
HomeCreateNotePage(editNoteId: noteId),加载原有标题、正文、媒体、隐私设置; - 编辑返回后自动刷新草稿列表并同步草稿数量。
- 分页加载:滚动到底部 180px 内自动加载更多。
展览草稿:
- 懒加载策略:首次切换到「展览」Tab 时才触发数据拉取,减少首屏开销。
- 展示形式:双列网格(
177:181),与已发布展览卡片视觉一致。 - 继续布展:点击草稿卡片后:
- 调用
getExhibitionDetail(id)拉取完整展览详情; - 通过
fastExhibitDraftProvider.notifier.loadFromExhibition(detail)将草稿数据回填到快布展状态; - 导航至
FastExhibitPage,用户可在原有基础上继续编辑、增删展厅/展位、更换作品; - 返回草稿箱后自动刷新列表。
- 调用
- 空状态:「暂无展览草稿,保存展览草稿后会显示在这里」。
八、设置与系统
8.1 设置页(SettingsPage)
- 卡片式设置列表:圆角 12px 白色卡片,分组展示「个人信息」「当前版本」「关于一起展」。
- 注销账号:独立卡片,带警示文案「注销后账号无法恢复,请谨慎操作」。
- 退出登录:底部全宽按钮,点击后清理 Token、上报 Matomo 埋点、跳转登录页并清空路由栈。
8.2 个人信息页(PersonalInfoPage)
- 头像展示区:114px 大圆形头像,使用
CachedNetworkImage加载;右下角悬浮相机图标(28px 圆形白底),提示可编辑;点击跳转「编辑资料」页。 - 信息列表:白色卡片圆角 12px,展示「昵称」与「简介」两项,每项带标签(浅灰色 14px)+ 值(深灰色 14px)+ 右侧箭头;未设置时显示「未设置」占位文案。
- 跳转逻辑:点击昵称或简介均跳转
HomeEditProfilePage,用户可在同一页面完成两项编辑。
8.3 关于页(AboutPage)
- 品牌区:顶部居中展示 App Logo(108px 圆角方形,
assets/logo.jpg)+ 应用名称「一起展」(18px 加粗)。 - 版本信息:圆角 12px 白色卡片,展示「当前版本」与「版本更新」;版本更新项带深蓝色标签(
#1C2F5D)展示最新版本号,点击提示「已是最新版本」。 - 法律合规:独立白色卡片展示「用户协议」「隐私政策」,点击唤起对应协议页。
- 底部版权:页脚展示「@ 2025-2026 上海艾魅之文化艺术有限公司. All Rights Reserved」+ ICP 备案号「沪ICP备2025146963号-7A」+ 版本号,字体 11px 灰色,体现平台合规性。
8.4 账号注销页(AccountDeletePage)
- 警示性页面,引导用户了解注销后果,确认后执行账号注销流程。
九、扫码功能(QrScanPage)
- 使用
mobile_scanner实现二维码扫描。 - 权限处理:首次进入检查相机权限,被拒绝时展示「需要摄像头权限」引导页,提供「重试」与「去设置」两个选项。
- 扫描界面:黑色背景,中央挖空矩形框(
PathFillType.evenOdd实现遮罩),白色边框 + 圆角 16;顶部 AppBar 支持手电筒开关与前后摄像头切换。 - 结果处理:扫码结果若是 URL 则尝试外部浏览器打开;否则弹窗展示文本内容;处理完成后自动关闭扫描页。
十、设计与交互细节汇总
10.1 视觉一致性
- 主题系统:所有颜色、文字样式、间距均沉淀在
design_system包的AppColors、AppTextStyles、AppDimens中,严禁使用魔法数字。 - 渐变运用:登录按钮紫蓝渐变、发布按钮深蓝渐变、底部栏渐变、展厅信息卡渐变——渐变方向与色值经过精确计算,强化品牌质感。
- 圆角体系:不同场景使用不同圆角半径(小至 2px 的 Chip,大至 24px 的展厅卡,999px 的圆形头像),层次分明。
10.2 动效与微交互
- 点赞动画:双击视频/作品时红心浮起并淡出。
- 标题滚动:展览页超长标题单向循环滚动,避免截断又不占用额外空间。
- 状态切换过渡:Tab 指示器、Chip 选中态、按钮态均有过渡动画(
AnimatedOpacity、AnimatedContainer)。 - 图片加载占位:统一使用
AppImageLoadingPlaceholder,避免布局跳动。
10.3 性能优化
- 图片分级加载:
thumbnailLevel机制(0=原图/视频,1=大图,2=中图,3=小图,9=笔记轮播缩略图),不同场景按需加载不同尺寸。 - 视频预加载:视频频道提前预热下一条视频控制器。
- Riverpod 状态共享:点赞、收藏等交互状态通过全局
noteEntityProvider共享,避免页面间状态不同步。 - RepaintBoundary:复杂独立图形区域使用
RepaintBoundary减少不必要的重绘。
10.4 安全与隐私
- 协议前置:登录、注册必须明确同意用户协议与隐私政策。
- 隐私分级:笔记发布支持「部分可见/不让谁看」的细粒度权限控制,联系人选择支持搜索与多选。
- 实名认证:办展功能需完成实名认证,未认证用户点击办展触发弹窗引导。
10.5 异常与降级
- 视频加载失败:降级为封面图展示。
- 网络证书错误:登录/验证码场景对
HandshakeException给出明确中文提示。 - 空状态:所有列表均配备空状态占位(
AppEmptyPlaceholder),带图标与引导文案。 - 认证过期:个人主页监听
AuthExpiredException,弹窗引导重新登录,登录成功后自动刷新全部数据。
十二、全局实体状态管理与跨页面同步
问题背景:在传统 Flutter 开发中,跨页面的点赞、收藏、关注状态很容易因各自维护独立状态而失步。用户在一个页面点赞后,回到列表页或进入另一个页面,状态可能回退到旧值。
「一起展」的解决方案:
- 全局实体仓库:在
shared_models中定义noteEntityProvider(StateProvider.family<NoteEntity?, int>)与exhibitionEntityProvider,以笔记/展览 ID 为 family key,任何页面均可通过同一 Provider 读写同一实体的状态。 - 批量同步机制:
batchUpsertNoteEntities(ref, entities)与batchUpsertExhibitionEntities(ref, entities)在列表页收到接口返回后,一次性将所有条目注入全局仓库;详情页、交互控制器则从同一仓库读取最新状态。 - 交互控制器架构:
noteInteractionControllerProvider:封装点赞/取消点赞逻辑,成功后更新全局noteEntityProvider中对应实体的isLiked与likeCount;exhibitionInteractionControllerProvider:同理处理展览的点赞与收藏。
- 状态持久化效果:用户在看展页点赞某作品 → 退出看展页 → 再次进入同一展览,点赞状态保持红色高亮,不会回退到初始值。
- 跨页面即时同步:用户在笔记详情页点赞 → 返回首页瀑布流 → 列表中该笔记的点赞数与红心状态已同步更新(因首页
didPopNext刷新拉取最新数据,同时全局仓库保证了无刷新时的状态一致性)。
十三、音频焦点与全局媒体协调
问题背景:移动设备同时只能有一个音频焦点。当用户在看展页播放背景音乐,又切换到笔记详情页观看视频,或者进入音频展览页播放语音导览时,必须有一套协调机制防止多个音源互相抢占、混乱播放。
「一起展」的协调策略:
- GlobalMusicService:基于
just_audio封装的全局音乐服务,所有非视频类音频(展厅背景音乐、音频展览)统一由此服务播放。 - MediaPlaybackCoordinator:视频播放器(
video_player)在初始化/播放前向协调器注册;协调器确保同一时刻只有一个视频在播放;新视频开始播放时,自动暂停其他视频与背景音乐。 - 音频焦点抢占与恢复:
- 视频播放 → 自动暂停
GlobalMusicService; - 视频结束/离开视频页 → 若当前页有背景音乐需求,自动恢复播放;
- 展厅背景音乐开启 → 视频预加载但不自动播放,等待用户手动点击后才抢占焦点。
- 视频播放 → 自动暂停
- 展厅内音乐状态精细管理:
- 进入作品详情页或放大预览 →
_pauseMusicTemporarily()暂停音乐,同时更新_currentMusicPlaying = false; - 返回展厅 →
_resumeMusicTemporarily()恢复音乐,同时更新_currentMusicPlaying = true; - 避免因状态缓存过期导致音乐按钮点击无效(
_scheduleSyncMusic中每次校验_musicHandle.isPlaying,而非依赖过期的本地布尔值)。
- 进入作品详情页或放大预览 →
- Android 音频焦点不中断:
VideoPlayerController增加VideoPlayerOptions(mixWithOthers: true),避免 Android 系统强制抢占音频焦点导致背景乐被系统级中断后无法恢复。
十四、展览封面解析策略
封装位置:lib/features/exhibit/domain/exhibit_cover_resolver.dart
解析规则(体现对多媒体混合场景的细致处理):
- 传入
files列表(含fileId+fileType)。 - 首项为视频(
fileType==2):仅使用视频封面(thumbnailLevel=2),不再额外拼接图片,避免视频封面与静态图风格不一致造成视觉跳跃。 - 首项为图片(
fileType==1):提取前 2 张图片(自动跳过视频),用于封面轮播;若不足 2 张则有几张展示几张。 - 海报优先:优先使用
visitFileId作为海报;若展位首项为图片,则海报 + 首张展位图轮播;若首项为视频,则仅展示海报。 - 接入点:已统一接入
ExhibitRepository的展览列表、我的展览列表、模板列表组装逻辑,确保全 App 封面展示规则一致。
十五、一些设计与优化
| 优化项 | 问题 | 解决方案 |
|---|---|---|
| 状态栏自适应 | 从视频/看展页返回首页后,状态栏图标颜色残留,导致在白底上不可见 | 首页根节点包裹 AnnotatedRegion<SystemUiOverlayStyle>,视频 Tab 强制 light,其余强制 dark |
| 展厅分组规则 | 旧逻辑要求 boothNo 必须完整覆盖 1..boothCount,导致合法但不连续的展位无法渲染 | 放宽为:只要存在合法 boothNo 即渲染;fileId 为空的展位占位显示;仅当所有 booth fileId 为空时整厅不渲染 |
| 交互按钮不可点 | 自动预览蒙层 Positioned.fill + GestureDetector(opaque) 覆盖了底部点赞/评论/收藏栏 | 限制蒙层 bottom: MediaQuery.padding.bottom + 64,仅覆盖内容区,避开底部操作栏 |
| 音乐按钮状态失步 | 缓存的 _currentMusicPlaying 布尔值与真实音频播放状态不一致,导致点击无效 | 每次同步时以 _musicHandle.isPlaying 为准;临时暂停/恢复时同步更新本地状态 |
| 点赞状态回退 | 离开展厅再进入,点赞状态回退到初始值 | 全部迁移到 Riverpod 全局实体仓库,以真实 ID 为键持久化交互状态 |
| 视频预加载 | 滑动切换视频时频繁出现黑屏/卡顿 | 开始播放约 5 秒后预热下一条视频的 VideoPlayerController,优先保证滑动流畅 |
| 动态展位布局 | 发布页固定 5 个图位,无法适配不同模板的展位数量差异 | 按模板 boothList.length 动态生成展位,提交时也按实际展位数提交 boothList |
| 封面解析一致性 | 不同页面展览封面展示规则不一,视频封面与图片混排时视觉混乱 | 统一封装 exhibit_cover_resolver,按 fileType 优先级提取,全 App 复用同一策略 |
| 展厅信息卡设计 | 策展人姓名蓝色下划线过于花哨,干扰阅读;音乐按钮透明层模糊不清晰 | 策展人姓名仅加粗无下划线;音乐按钮改为实底设计,提高可识别度 |
| 发布成功后路由 | 发布成功或保存草稿后,用户迷失在布展流程中,不知道回到了哪里 | 发布成功 → 首页「我的」Tab;保存草稿 → 草稿箱「展览」Tab,明确告知用户内容归属 |
十六、路由体系(AppRoutes / AppRouter)
架构原则:壳工程 host_app 集中注册所有路由,各 feature_* 包仅依赖 core_router 的抽象,禁止互相 import 其他 feature 的 Page 类。
已注册路由清单:
| 路由名 | 页面 | 关键参数 |
|---|---|---|
splash | SplashPage | — |
login | LoginPage | — |
bindPhone | BindPhonePage | — |
agreement | AgreementPage | type(AgreementType) |
home | HomePage | — |
homeCreateNote | HomeCreateNotePage | — |
homeDrafts | HomeDraftListPage | initialTabIndex(0=笔记,1=展览) |
noteDetail | NoteDetailPage | noteId, startInPreview, enableBackToPreviewWhenTextMode |
audioExhibitPublish | AudioExhibitPublishPage | — |
audioExhibitView | AudioExhibitViewPage | coverUrl, title, artist |
fastExhibit | FastExhibitPage | — |
exhibitTab | ExhibitTabPage | — |
exhibitCoverTemplate1 | ExhibitCoverPageTemplate1 | hall(ExhibitHall) |
exhibitDetailTemplate1 | ExhibitDetailPageTemplate1 | — |
exhibitDetailType1/2/3 | 多种展览详情模板 | — |
exhibitViewingTemplate1 | ExhibitViewingPageTemplate1 | title, exhibitionId, initialDetail |
exhibitArtworkDetailTemplate1 | ExhibitArtworkDetailPageTemplate1 | 作品相关全量参数(imageUrl, visitFileId, artworkAuthor 等) |
exhibitArtworkCommentTemplate1 | ExhibitArtworkCommentPageTemplate1 | title, commentCount, exhibitionId, boothId |
exhibition3d | Exhibition3dPage | — |
userProfile | UserProfilePage | userId |
mySettings | SettingsPage | — |
myPersonalInfo | PersonalInfoPage | — |
myAbout | AboutPage | — |
myAccountDelete | AccountDeletePage | — |
参数传递规范:
- 所有参数统一通过
Map<String, Object?>传递; _args()辅助函数自动转换参数类型,兼容Map与Map<String, Object?>;- 每个路由构造器负责从
args中提取字段并赋予默认值,避免空值崩溃。
十七、设计系统组件(Design System)
packages/common/design_system 沉淀了全 App 复用的视觉原子与分子组件,确保跨 feature 体验一致性。
17.1 主题 Token
- AppColors:全部颜色常量(
textPrimary,textSecondary,textTertiary,buttonPrimary,link,inputBorder,surface,backgroundSecondary等),无魔法数字。 - AppTextStyles:按字号/字重/行高分级(
loginTitleText28,body12,body14,body16,fastExhibitTitle,button16等),保证全 App 字体层级统一。 - AppDimens:间距、圆角、高度等维度常量(
pageHorizontalPadding,exhibitBannerHeight,exhibitContentTopOverlap,inputRadius等)。
17.2 通用组件
- AppEmptyPlaceholder:空状态占位,支持自定义标题、副标题、图标;全 App 列表空状态统一使用。
- AppSkeletonHallGrid:骨架屏网格,用于展览/笔记列表首次加载时的占位,减少白屏焦虑。
- AppImageLoadingPlaceholder:图片加载中占位,统一灰色背景 + 加载图标,避免布局跳动。
- AppModalSheet:底部弹窗封装,支持
blurSigma毛玻璃效果、圆角、自定义内容区;用于作品信息、筛选、分享等场景。 - AppTopToast:顶部轻提示,用于操作成功/失败的非阻塞反馈(如「已是最新版本」「加载更多失败」)。
17.3 表单与输入
- DashedBorder:虚线边框容器,用于海报/封面上传区的「点击上传」视觉引导。
- AppMediaPicker:统一封装图片/视频/音频选择器,支持拍照、相册、文件选择,供所有 feature 复用。
十八、埋点与分析(Matomo)
集成方式:core_analytics 包封装 Matomo 上报,提供 AppAnalyticsService.instance.trackEvent() 与 trackPageView()。
埋点覆盖场景:
- 页面浏览:所有路由跳转均携带
matomoTitle(如「首页-推荐」「办展分类」「笔记详情」),用于漏斗分析。 - 用户行为:登录/登出、点赞/取消点赞、收藏、评论、分享、发布笔记/展览、保存草稿。
- 视频播放:视频频道自动上报播放时长、播放错误率、播放完成率。
- 布展漏斗:进入办展分类 → 点击模板 → 开始布展 → 填写信息 → 发布成功,全流程追踪转化率。
- 认证过期:
AuthExpiredException触发时单独上报,用于监控登录态稳定性。
隐私合规:
- 用户登出时调用
clearVisitorUser(),清除 Matomo 访客标识; - 所有事件 category/action/name 均使用英文枚举,便于国际化团队理解。
十九、分享系统(AppShareSheet / AppShareService)
产品定位:分享是内容社区的核心传播链路,「一起展」为笔记、展览、作品、个人主页均提供了统一的分享能力。
UI 组件(AppShareSheet):
- 底部弹窗:
showModalBottomSheet实现,顶部圆角 16px,毛玻璃背景(BackdropFilter,sigmaX: 16,sigmaY: 16),高度固定 254px; - 标题栏:左侧「分享」标题(20px 加粗),右侧圆形关闭按钮(28px,浅灰背景 + 深色关闭图标);
- 分享渠道:横向滑动排列的渠道图标:
- 微信:54px SVG 图标 + 13px 标签;
- 朋友圈:同上;
- 复制链接:54px 圆形灰底图标 + 链接符号;
- 未启用的渠道(私信/微博/QQ 空间)置灰处理(
opacity: 0.4),点击提示「该分享渠道暂未开放」。
- 渠道状态管理:通过
enabledChannels参数灵活控制哪些渠道可用,不同场景可差异化配置。
分享服务(AppShareService):
- 注册机制:
registerGlobalHandler()在 App 启动时注入全局分享处理器,实现 feature 包与具体分享 SDK 的解耦; - 微信分享:基于
wechat_kit实现,支持分享到微信会话(kSession)和朋友圈(kTimeline); - 链接复制:调用
Clipboard.setData将分享链接复制到剪贴板,成功后提示「链接已复制」;
分享链接构建(ShareUrlBuilder):
- 统一域名:
https://app.amaz-cn.com/app/share/,便于 Deeplink 解析与追踪; - 支持类型:笔记详情、展览观看、作品详情、用户主页;
- 参数结构:
?type=xxx¬eId=xxx&exhibitionId=xxx&boothId=xxx&userId=xxx; - 解析能力:
ShareUrlBuilder.parse(Uri)可反向解析分享链接,用于 Deeplink 跳转(如从微信 H5 唤醒 App 并直达内容页)。
使用场景:
- 笔记详情页底部栏 → 分享笔记;
- 展览浏览页 → 分享展览;
- 作品详情页 → 分享单幅作品;
- 个人主页 → 分享用户主页。

注:本文档基于代码遍历整理,后续可根据实际产品迭代持续补充完善。